﻿// Module:             Games Programming
// Assignment1:        Platformer Game
// Add game feature:   Level. Manages everything inside a level, plus ability to save/load
// Student Number:     S10096666C
// Student Name:       Trinh Quang Anh

//////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Game.EnemyScripts;
using Game.Platforms;
using Game.TrapScripts;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Game
{
    // NOTE: Coding style is Camel Case
    // Rule of first letters:
    // private - small case prefixed with underscore
    // public properties - uppercase
    // local var - small case
    public class Level : IDisposable
    {
        private readonly List<IEnemy> _enemies = new List<IEnemy>();
        private readonly List<ITrap> _traps = new List<ITrap>();
        private readonly List<Gem> _gems = new List<Gem>();
        private readonly List<IPlatform> _platforms = new List<IPlatform>();
        private readonly ContentManager _contentManager;

        private readonly Player _player;
        private readonly Camera _camera;
        private Rectangle _exitRect;
        private bool _reachedExit;
        private TimeSpan _timeElapsed;
        private readonly SpriteFont _font;

        private int _score;

        private readonly string _levelName;

        private readonly int _viewportWidth;
        private readonly int _viewportHeight;

        private readonly float _lowestY = 500; // temp

        private readonly Texture2D _background;
        private Vector2 _backgroundScroll, _lastCamPos, _halfViewport;
        private bool _isFirstUpdate = true;
        private SoundEffectInstance _bgmi;

        public Level(IServiceProvider serviceProvider, string mapName, int viewportWidth, int viewportHeight)
        {
            _levelName = mapName;
            _reachedExit = false;
            _contentManager = new ContentManager(serviceProvider, "Content");

            _player = new Player(this);

            _viewportWidth = viewportWidth;
            _viewportHeight = viewportHeight;
            _camera = new Camera(viewportWidth, viewportHeight);

            //_font = Content.Load<SpriteFont>("debug");
            LoadMap(mapName);

            _background = Content.Load<Texture2D>("Sprites/starfield");
            _backgroundScroll = new Vector2(_viewportWidth, _viewportHeight);
            _lastCamPos = Player.Position;
            _halfViewport = new Vector2(_viewportWidth/2, _viewportHeight/2);
            //_bgDestRect = new Rectangle(0, 0, _viewportWidth, _viewportHeight);
            Texture2D a = Content.Load<Texture2D>("Sprites/Enemy/SpaceInvader");

            var bgm = Content.Load<SoundEffect>("Sound Effect/BGM_1");
            _bgmi = bgm.CreateInstance();
            _bgmi.IsLooped = true;
            _bgmi.Volume = 0.4f;

            //IEnemy u = new LaserShooter(this, new Vector2(0, -200), 5000, a,a);
            //Enemies.Add(u);
        }

        #region Properties
        public ContentManager Content
        {
            get { return _contentManager; }
        }

        public List<IPlatform> Platforms
        {
            get { return _platforms; }
        }

        public Camera Camera
        {
            get { return _camera; }
        }

        public List<IEnemy> Enemies
        {
            get { return _enemies; }
        }

        public Player Player
        {
            get { return _player; }
        }

        public bool ReachedExit
        {
            get { return _reachedExit; }
        }

        public int Score
        {
            get { return _score; }
        }

        public string LevelName
        {
            get { return _levelName; }
        }

        public List<ITrap> Traps
        {
            get { return _traps; }
        }

        public Rectangle ExitRect
        {
            get { return _exitRect; }
        }

        public List<Gem> Gems
        {
            get { return _gems; }
        }
        #endregion
        #region IDisposable Members

        public void Unload()
        {
            Enemies.Clear();
            Platforms.Clear();
            _bgmi.Stop(true);
            GC.Collect(GC.MaxGeneration);
        }

        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        #endregion

        // Platform type for now is 1
        public void AddPlatform(PlatformType type, int mouseX, int mouseY)
        {
            Camera.GetTranformation();
            var texture = Content.Load<Texture2D>("Sprites/Platforms/" + type.ToString());
            
            var pos = new Vector2(mouseX, mouseY);
            pos.X = pos.X - texture.Width / 2f - _viewportWidth / 2f + _camera.Position.X;
            pos.Y = pos.Y - texture.Height / 2f - _viewportHeight / 2f + _camera.Position.Y;

            IPlatform platform = null;
            switch (type)
            {
                case PlatformType.Flat:
                    platform = new FlatPlatform();
                    break;
                case PlatformType.Elevator:
                    // NOTE: speed is defined as how many pixels per second
                    platform = new Elevator(this, 100f, 100f);
                    break;
            }

            if (platform != null)
            {
                platform.Spawn(type, texture, pos);
                Platforms.Add(platform);
            }
            
        }

        public void Update(GameTime gameTime, KeyboardState keyboardState, MouseState mouseState)
        {
            // Update the world
            foreach (var platform in Platforms)
                platform.Update(gameTime);

            foreach (IEnemy e in Enemies)
                e.Update(gameTime);

            foreach (ITrap t in Traps)
                t.Update(gameTime);

            UpdateGems();

            if (Player.IsAlive && !ReachedExit)
            {
                Player.Update(gameTime, keyboardState, mouseState);

                if (Player.Position.Y > _lowestY)
                {
                    Player.OnPlayerKilled();
                    return;
                }

                if (Player.IsOnGround)
                {
                    var depth = Player.BoundingRectangle.GetIntersectionDepth(ExitRect);
                    if (depth != Vector2.Zero)
                    {
                        _reachedExit = true;
                        Player.OnReachedExit();
                    }
                }
            }
            else if (!Player.IsOnGround)
            {
                Player.ApplyPhysics(gameTime);
            }

            if (_isFirstUpdate)
            {
                _lastCamPos = Player.Position;
                _bgmi.Play();
                _isFirstUpdate = false;
            }
        }
        
        private void UpdateGems()
        {
            //throw new NotImplementedException();
            for (int i = 0; i < Gems.Count; i++)
            {
                Gem g = Gems[i];

                Vector2 depth = g.BoundingRect.GetIntersectionDepth(Player.BoundingRectangle);
                if (depth != Vector2.Zero)
                {
                    Gems.RemoveAt(i);

                    _score += 100;
                    // TODO: play sound
                }
            }
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            var transform = Camera.GetTranformation();
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, null, transform);
            Vector2 topleft = Camera.Position - _halfViewport;

            if (Math.Abs(Camera.Position.X - _lastCamPos.X) > 1f)
            {
                _backgroundScroll -= Camera.Position - _lastCamPos;

                _backgroundScroll.Y = 0;

                if (_backgroundScroll.X <= 0)
                    _backgroundScroll.X = _viewportWidth;

                if (_backgroundScroll.X > _viewportWidth)
                    _backgroundScroll.X = 0;

                _lastCamPos = Camera.Position;
            }
            var temp = topleft +_backgroundScroll;
            spriteBatch.Draw(_background, temp, Color.White);

            temp.X -= _viewportWidth;
            spriteBatch.Draw(_background, temp, Color.White);
            spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, null, null, null, null, transform); 
            foreach (var platform in Platforms)
                platform.Draw(spriteBatch);
            spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, null, null, null, null, transform); 
            foreach (var gem in Gems)
                gem.Draw(gameTime, spriteBatch);
            spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, null, null, null, null, transform); 
            foreach (var enemy in Enemies)
                enemy.Draw(gameTime, spriteBatch);
            spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, null, null, null, null, transform); 
            foreach (var trap in Traps)
                trap.Draw(gameTime, spriteBatch);
            spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, null, null, null, null, transform); 
            Player.Draw(gameTime, spriteBatch);
            spriteBatch.End();
        }


        #region "Save and Load"
        /*
         * Content of map file:
         * Platforms -> Gems -> Enemies
         * For platforms:
         * one line for block type integer, followed by a line of integers, comma separated
         * each triplet of int represents one platform's type and coordinates (tile).
         * This representation was chosen for smaller map file,
         * fast parsing, and because map tiles cannot overlap each other
         */
        // FIXME: Avoid throwing Exceptions as they are bad for performance 
        private void LoadMap(string mapName)
        {
            string path = string.Format("Content/Levels/{0}.txt", mapName);
            var reader = new StreamReader(TitleContainer.OpenStream(path));

            LoadPlatforms(reader);
            LoadGems(reader);
            LoadEnemies(reader);
            LoadTraps(reader);
        }

        private void LoadPlatforms(StreamReader reader)
        {
            // Platforms
            string line = reader.ReadLine();
            if (line != "Platforms")
                throw new Exception("Wrong map format.");

            line = reader.ReadLine();
            var values = line.Split(',');

            if (values.Length % 3 != 0)
                throw new Exception("strange number of integer values");

            for (int i = 0; i < values.Length; i += 3)
            {
                // FIXME: use custom parsing for high performance. Input is almost guaranteed to be correct
                // Reference: http://www.dotnetperls.com/int-parse-optimization
                var type = (PlatformType)int.Parse(values[i]);
                int x = int.Parse(values[i + 1]);
                int y = int.Parse(values[i + 2]);
                var texture = Content.Load<Texture2D>("Sprites/Platforms/" + type.ToString());

                IPlatform platform = null;
                switch (type)
                {
                        
                    case PlatformType.Start:
                        Player.Reset(new Vector2(x*Game1.GridSize + 80 , y*Game1.GridSize - 64));
                        platform = new FlatPlatform();
                        platform.Spawn(type, texture, x, y);
                        break;
                    case PlatformType.VerticalFlat:
                    case PlatformType.Flat3:
                    case PlatformType.Flat1:
                    case PlatformType.Flat:
                        platform = new FlatPlatform();
                        platform.Spawn(type, texture, x, y);
                        break;
                    case PlatformType.Elevator:
                        platform = new Elevator(this, 160, 80f);
                        platform.Spawn(type, texture, x, y);
                        break;
                    case PlatformType.Disappearing:
                        platform = new DisappearingPlatform();
                        platform.Spawn(type, texture, x, y);
                        break;

                    case PlatformType.Exit:
                        platform = new FlatPlatform();
                        platform.Spawn(type, texture, x, y);
                        var r = platform.BoundingRect;
                        
                        // make it in suck a way that the exit rectangle is just 1 pixel bigger that the platform,
                        // so it can be correctly detected when player hit the exit.
                        _exitRect = new Rectangle(r.X -1, r.Y -1, r.Width+2, r.Height+2);
                        
                        break;
                    default:
                        platform = new InfoPlatform();
                        platform.Spawn(type, texture, x, y);
                        break;
                } 
                
                if (platform != null)
                {
                    Platforms.Add(platform);
                    // NOTE: DO NOT delete this line. Null check used to make sure that loading goes right
                    platform = null;
                }
                
            }
        }


        private void LoadEnemies(StreamReader reader)
        {
            string line = reader.ReadLine();
            if (line != "Enemies")
                throw new Exception("Wrong map format.");

            line = reader.ReadLine();
            if (line == null || line == "none")
                return;

            var values = line.Split(',');

            if (values.Length % 3 != 0)
                throw new Exception("strange number of integer values");

            for (int i = 0; i < values.Length; i += 3)
            {
                // FIXME: use custom parsing for high performance. Input is almost guaranteed to be correct
                // Reference: http://www.dotnetperls.com/int-parse-optimization
                var type = (EnemyType)int.Parse(values[i]);
                int x = int.Parse(values[i + 1]);
                int y = int.Parse(values[i + 2]);
                var position = new Vector2(x * Game1.GridSize, y * Game1.GridSize);
                Texture2D texture;

                IEnemy enemy = null;
                switch (type)
                {
                    case EnemyType.LeftRight:
                        texture = Content.Load<Texture2D>("Sprites/Enemy/SpaceInvader");
                        enemy = new LeftRightEnemy(position, 180f, 1f, texture);
                        break;

                    case EnemyType.UpDown:
                        texture = Content.Load<Texture2D>("Sprites/Enemy/SpaceInvader");
                        enemy = new UpDownEnemy(position, 180f, 1f, texture);
                        break;

                    case EnemyType.Chasing:
                        //TODO
                        texture = Content.Load<Texture2D>("Sprites/Enemy/SpaceInvaderAnimated");
                        enemy = new ChasingEnemy(this, position, 80f, texture);
                        break;
                }
                
                if (enemy!= null)
                {
                    Enemies.Add(enemy);
                    // NOTE: DO NOT delete this line. Null check used to make sure that loading goes right
                    enemy = null;   
                }
            }
            
        }

        private void LoadTraps(StreamReader reader)
        {
            string line = reader.ReadLine();
            if (line != "Traps")
                throw new Exception("Wrong map format.");

            line = reader.ReadLine();
            if (line == null || line == "none")
                return;

            string[] values = line.Split(',');

            if (values.Length % 3 != 0)
                throw new Exception("strange number of integer values");

            for (int i = 0; i < values.Length; i += 3)
            {
                var type = (TrapType)int.Parse(values[i]);
                int x = int.Parse(values[i + 1]);
                int y = int.Parse(values[i + 2]);
                var position = new Vector2(x * Game1.GridSize, y * Game1.GridSize);
                Texture2D texture; 

                ITrap trap = null;
                switch (type)
                {
                    case TrapType.LaserDoor:
                        texture = Content.Load<Texture2D>("Sprites/Trap/Laser");
                        trap = new LaserDoor(position, 1.5f, 1.5f, texture, Content.Load<Texture2D>("Sprites/Trap/LaserBG"));
                        break;
                    case TrapType.Spikes:
                        texture = Content.Load<Texture2D>("Sprites/Trap/spikes");
                        trap = new Spikes(texture, x, y);
                        break;
                }

                if (trap != null)
                {
                    Traps.Add(trap);
                    trap = null;
                }
                
            }
        }

        private void LoadGems(StreamReader reader)
        {
            string line = reader.ReadLine();
            if (line != "Gems")
                throw new Exception("Wrong map format.");

            line = reader.ReadLine();
            if (line == null || line == "none")
                return;

            var values = line.Split(',');

            if (values.Length % 2 != 0)
                throw new Exception("strange number of integer values");

            var texture = Content.Load<Texture2D>("Sprites/Gem/Gem");

            for (int i = 0; i < values.Length; i+=2)
            {
                int x = int.Parse(values[i]);
                int y = int.Parse(values[i + 1]);
                
                Gems.Add(new Gem(texture, x, y));
            }
        }

        public void SaveMap()
        {
            // more efficient than using plus, but seriously need testing.
            // calling a method 6 times each loop is BAD. Too much call overhead.
            var builder = new StringBuilder();

            // Platforms
            builder.AppendLine("Platforms");

            foreach (IPlatform platform in Platforms)
            {   
                var type = (int) platform.Type;
                int x = (int) platform.Position.X/32;
                int y = (int) platform.Position.Y/32;
                builder.Append(type);
                builder.Append(',');
                builder.Append(x);
                builder.Append(',');
                builder.Append(y);
                builder.Append(',');
            }

            builder.AppendLine();

            // Gems
            builder.AppendLine("Gems");

            foreach (Gem gem in Gems)
            {
                int x = (int) gem.Position.X/32;
                int y = (int) gem.Position.Y/32;
                builder.Append(x);
                builder.Append(',');
                builder.Append(y);
                builder.Append(',');
            }

            builder.AppendLine();

            // Enemies
            /*
            builder.AppendLine("Enemies");

            foreach (EnemySample enemy in _enemies)
            {
                var type = (int) enemy.Type;
                int x = (int) enemy.Position.X/32;
                int y = (int) enemy.Position.Y/32;
                builder.Append(type);
                builder.Append(',');
                builder.Append(x);
                builder.Append(',');
                builder.Append(y);
                builder.Append(',');
            }*/
        }
#endregion
    }
}